Go 的 defer 坑
defer 函数压栈
这个关键字类似 Java 中的 finally
关键字,会在 try
代码块的最后执行,这个 defer
在 return
之后执行
import "fmt"
func main() {
defer fmt.Println("main end~")
fmt.Println("main 1")
fmt.Println("main 2")
}
打印结果
main 1
main 2
main end~
它的执行其实是压栈的过程,如下代码,逐步压栈
import "fmt"
func fun01() {
fmt.Println("A")
}
func fun02() {
fmt.Println("B")
}
func fun03() {
fmt.Println("C")
}
func main() {
defer fun01()
defer fun02()
defer fun03()
}
打印结果
C
B
A
defer 的执行坑
题目一
题目说明:函数 deferFuncParameter()
定义一个整型变量并初始化为 1,然后使用 defer 语句打印出变量值,最后修改变量值为 2
func deferFuncParameter() {
var aInt = 1
defer fmt.Println(aInt)
aInt = 2
return
}
参考答案:
输出 1。延迟函数 fmt.Println(aInt)
的参数在 defer 语句出现时就已经确定了,所以无论后面如何修改 aInt 变量都不会影响延迟函数。
题目二
函数 deferFuncParameter()
定义一个数组,通过 defer 延迟函数 printArray()
的调用,最后修改数组第一个元素。printArray()
函数接受数组的指针并把数组全部打印出来。
package main
import "fmt"
func printArray(array *[3]int) {
for i := range array {
fmt.Println(array[i])
}
}
func deferFuncParameter() {
var aArray = [3]int{1, 2, 3}
defer printArray(&aArray)
aArray[0] = 10
return
}
func main() {
deferFuncParameter()
}
输出 10、2、3 三个值。延迟函数 printArray()
的参数在defer语句出现时就已经确定了,即数组的地址,由于延迟函数执行时机是在 return 语句之前,所以对数组的最终修改值会被打印出来。
如果改成下面这样则是 1、2、3 三个值,因为此时延迟函数的参数是数组的值,而不是地址。
func printArray(array [3]int) {
for i := range array {
fmt.Println(array[i])
}
}
func deferFuncParameter() {
var aArray = [3]int{1, 2, 3}
defer printArray(aArray)
aArray[0] = 10
return
}
func main() {
deferFuncParameter()
}
题目三
下面函数输出什么?
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
参考答案:函数输出 2。函数的 return 语句并不是原子的,实际执行分为 设置返回值-->ret,defer语句实际执行在返回前,即拥有 defer 的函数返回过程是:设置返回值-->执行defer-->ret
。
所以 return 语句先把 result 设置为 i 的值,即 1,defer 语句中又把 result 递增1,所以最终返回 2。
defer 规则
1、延迟函数的参数在defer语句出现时就已经确定下来了 2、延迟函数执行按后进先出顺序执行,即先出现的 defer 最后执行 3、延迟函数可能操作主函数的具名返回值
主要通过第三个规则来理解一下函数返回过程
有一个事实必须要了解,关键字 return 不是一个原子操作,实际上 return 只代理汇编指令 ret,即将跳转程序执行。比如语句 return i
,实际上分两步进行,即将 i 值存入栈中作为返回值,然后执行跳转,而 defer 的执行时机正是跳转前,所以说 defer 执行时还是有机会操作返回值的。
举个实际的例子进行说明这个过程:
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
该函数的 return 语句可以拆分成下面两行:
result = i
return
而延迟函数的执行正是在 return 之前,即加入 defer 后的执行过程如下:
result = i
result++
return
所以上面函数实际返回 i++ 值。
关于主函数有不同的返回方式,但返回机制就如上机介绍所说,只要把 return 语句拆开都可以很好的理解,下面分别举例说明
主函数拥有匿名返回值
一个主函数拥有一个匿名的返回值,返回时使用字面值,比如返回 "1"、"2"、"Hello" 这样的值,这种情况下 defer 语句是无法操作返回值的。
一个返回字面值的函数,如下所示:
func foo() int {
var i int
defer func() {
i++
}()
return 1
}
上面的 return 语句,直接把 1 写入栈中作为返回值,延迟函数无法操作该返回值,所以就无法影响返回值。
同样的,一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下 defer 语句可以引用到返回值,但不会改变返回值。
一个返回本地变量的函数,如下所示:
func foo() int {
var i int
defer func() {
i++
}()
return i
}
上面的函数,返回一个局部变量,同时 defer 函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为"anony",上面的返回语句可以拆分成以下过程:
anony = i
i++
return
由于 i 是整型,会将值拷贝给 anony,所以 defer 语句中修改 i 值,对函数返回值不造成影响。
主函数拥有具名返回值
主函声明语句中带名字的返回值,会被初始化成一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果 defer 语句操作该返回值,可能会改变返回结果。
一个影响函返回值的例子:
func foo() (ret int) {
defer func() {
ret++
}()
return 0
}
上面的函数拆解出来,如下所示:
ret = 0
ret++
return
函数真正返回前,在 defer 中对返回值做了 +1 操作,所以函数最终返回 1。